module server.ControlChannel;

import server.socket;
import server.TransferChannel;
import server.Timer;
import std.c.windows.windows;
import std.c.windows.winsock;
import winapi.winapi;
import std.file;
import std.path;
import std.stdio;
import std.stdarg;
import std.string;
import std.date;

class ControlChannel : Socket {
	override protected Socket accepting(SOCKET s) {	return new ControlChannel(s);	}	
	override protected this(SOCKET s) {	super(s); }	
	public this() {	super(); }
	
	private ubyte[] readbuf;
	private int msg_count = 0;
	private int[] trans_chans;
	private Transfer[int] transfers;
	static TransferChannel trans_chan = null;
	
	static const ushort EXCEPTION = 0;
	static const ushort LIST_DIR = 1;
	static const ushort TRANSFER_REQ = 2;
	static const ushort TRANSFER_ACK = 3;	
	static const ushort SCREEN_CAP = 4;
	static const ushort SEND_CLICK = 5;

	//callback functions
	override protected void on_read(ubyte[] buf) {
		readbuf ~= buf;
		
		uint msg_len;
		
		//message fields
		ushort msg_code;
		ubyte arg_name_len;
		uint arg_val_len;
		char[] arg_name;
		ubyte[] arg_val;
		bool invalid_msg;
		ubyte *ptr, end;		
		
		while (readbuf.length >= uint.sizeof)
		{
			//read the next message length
			msg_len = *cast(uint *)(readbuf.ptr);
			//make sure that...
			//a) the read buffer contains the entire message (msg_len + uint.sizeof)
			//b) the message is at least long enough to contain a message code & arg count ie it's valid (ushort.sizeof)*2
			if ((readbuf.length < msg_len+uint.sizeof) || (msg_len < ushort.sizeof*2))
				return;
			
			//get the entire message
			scope ubyte[] msg = readbuf[4..msg_len+4];
			readbuf = readbuf[msg_len+4..$];
			
			scope ubyte[][char[]]args;
			ptr = msg.ptr;
			end = ptr + msg.length;
			
			//process the message
			//keywords here are validation and security.			
			//see protocol.txt for protocol explanation		
			
			//get message code
			msg_code = *cast(ushort *)(ptr);
			ptr += ushort.sizeof;
	
			//process message argument dictionary				
			//very useful shorthand
			uint ptr_len() { return end - ptr; }
			
			invalid_msg = false;
			while (ptr < end) {
				//read argument name length
				arg_name_len = *ptr++;
				if (ptr_len() < arg_name_len)
					goto invalid_msg;
				//read argument name
				arg_name = cast(char[])ptr[0..arg_name_len];
				ptr += arg_name_len;
				
				//read argument value length
				if (ptr_len() < ushort.sizeof)
					goto invalid_msg;
				arg_val_len = *cast(ushort *)ptr;
				ptr += ushort.sizeof;
				//read argument value
				if (ptr_len() < arg_val_len)
					goto invalid_msg;
				arg_val = ptr[0..arg_val_len];
				ptr += arg_val_len;
				
				args[arg_name] = arg_val;
				continue;
				
				invalid_msg:
					//an invalid message has been passed. ignore this one
					invalid_msg = true;
					break;
			}
			if (invalid_msg)
			{
				//ignore messages with invalid format
				//TODO: remove debug statement
				writefln("DEBUG: invalid message presented");
				continue;
			}
			//pass the completed message to the read handler
			read_msg(msg_code, args);
		}
	}
	
	//helper functions
	public void write_msg(ushort code, ...) {
		scope ubyte[] buf;
		char []arg_name;
		ubyte []arg_val;
		TypeInfo name_t;
		TypeInfo arg_t;
		
		buf ~= get_bytes(code);
		if (_arguments.length % 2 != 0) {
			throw new Exception("invalid number of arguments in write_msg");
		}
		for (int i = 0;i < _arguments.length;i+=2)
		{
			name_t = _arguments[i];
			arg_t = _arguments[i+1];
			
			if (name_t != typeid(char[]))
				throw new Exception(
						std.string.format(
								"error in write_msg, argument %d should be a string, but is a %s",
								i, arg_t.toString()));
			arg_name = va_arg!(char[])(_argptr);
			
			if (arg_t == typeid(int)) 
				arg_val = get_bytes(va_arg!(uint)(_argptr));
			else if (arg_t == typeid(char[]))
				arg_val = cast(ubyte[])va_arg!(char[])(_argptr);
			else if (arg_t == typeid(ubyte[]))
				arg_val = va_arg!(ubyte[])(_argptr);
			else
				throw new Exception("invalid parameter type");
			
			buf ~= cast(ubyte)arg_name.length;
			buf ~= cast(ubyte[])arg_name;
			buf ~= get_bytes(cast(ushort)arg_val.length);
			buf ~= arg_val;
		}
		write(get_bytes(buf.length));
		write(buf);
	}
	
	private Transfer openTransfer(TransferType type = TransferType.Block, TransferDelegate dg = null, Object state = null)
	{
		if (trans_chan is null)
			trans_chan = new TransferChannel(this);
		Transfer tr = trans_chan.new_transfer(type, dg, state);
		transfers[tr.transfer_id] = tr;
		
		write_msg(TRANSFER_REQ, "trans_id", tr.transfer_id);
		return tr;
	}
	
	private Transfer openAsyncTransfer(TransferDelegate dg, Object state)
	{
		return openTransfer(TransferType.Async, dg, state);
	}
	
	public void raise(char[]msg)
	{
		raise(-1, msg);
	}
	
	public void raise(int code, char[]msg)
	{
		write_msg(EXCEPTION, "code", code, "msg", msg);
	}
	
	private void read_msg(uint msg, ubyte[][char[]]args) {
		/++
		writefln("recevied %d message", msg);
		writefln("arguments:");
		foreach (char[] k, ubyte[] v; args) {
			if (v.length == 4)
				writefln("%s:%s", k, to_int(v));
			else
				writefln("%s:%s", k, cast(char[])v);
			
		}
		writefln("--------");
		++/
		
		switch (msg)
		{
			//DIR LIST
			case LIST_DIR:
				debug writefln("LIST_DIR");
				dir_list(cast(char[])args["dir"]);
			break;
			
			//peer acknowledged our transfer, so open the connection
			case TRANSFER_ACK:
				debug writefln("TRANSFER_ACK");
				//determine which transfer was acknowledged then open the connection
				int trans_id = to_int(args["trans_id"]);
				Transfer c = transfers[trans_id];
				
				char[] peer = remote_addr();
				char[] host = peer[0..peer.find(':')];
				int port = atoi(peer[peer.find(':')+1..$])+1;
				
				//TODO: what if the transfer channel is already open?
				//connect the transfer channel
				c.channel.connect(host, port);
			break;
			
			case SCREEN_CAP:
				debug writefln("SCREEN_CAP");
				screen_cap(to_int(args["scale"]));
			break;
			
			case SEND_CLICK:
				debug writefln("SEND_CLICK");
				send_click(to_int(args["px"]), to_int(args["py"]));
			break;
			
			default:
		}
	}
	
	private void send_click(int x, int y)
	{
		//TODO: mouse click not working, r shows success
		INPUT a;
		a.type = INPUT_MOUSE;
		a.mi.dx = x;
		a.mi.dy = y;
		a.mi.mouseData = 0;
		a.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
		a.mi.time = 0;
		a.mi.dwExtraInfo = 0;
		
		int r = 
		SendInput(1, &a, a.sizeof);
		Sleep(1);
		a.mi.dwFlags = MOUSEEVENTF_LEFTUP;
		
		debug writefln("r = %d", r);
		r =
		SendInput(1, &a, a.sizeof);
		debug writefln("r = %d", r);
	}
	
	private void screen_cap(int scale)
	{
		if ((scale > 100) || (scale < 1))
		{
			raise("screen capture scale must be between 1 and 100");
			return;
		}
		HDC screen_dc = GetDC(null);
		int x = GetSystemMetrics (SM_CXSCREEN);
		int y = GetSystemMetrics (SM_CYSCREEN);
		
		uint dest_x = (x * scale) / 100;
		uint dest_y = (y * scale) / 100;
		
		writefln("resizing from [%d,%d] to [%d,%d]", x, y, dest_x, dest_y);
		
		HBITMAP bitmap = CreateCompatibleBitmap(screen_dc, x, y);
		HDC dest_dc = CreateCompatibleDC(screen_dc);
		HBITMAP dest_bm = CreateCompatibleBitmap(dest_dc, dest_x, dest_y);
		
		HDC mem_dc = CreateCompatibleDC(screen_dc);
		HGDIOBJ old_obj = SelectObject(mem_dc, bitmap);
		old_obj = SelectObject(dest_dc, dest_bm);
		
		SetStretchBltMode(mem_dc, 4);
		int r =
			BitBlt(
				mem_dc, //destination
				0, 0,
				x, y,
				screen_dc,
				0, 0,				
				SRCCOPY);
		debug writefln("r = %d GLE: %d", r, GetLastError());
		
		r =
			StretchBlt(
				dest_dc, //destination
				0, 0,
				dest_x, //width
				dest_y, //height
				mem_dc, //source
				0, 0, 
				x, y,
				SRCCOPY
				);
		debug writefln("r = %d GLE: %d", r, GetLastError());
		
		ubyte[] buffer = new ubyte[dest_x*dest_y*3];
		BITMAPINFO bmi;
		std.c.string.memset(&bmi, 0, bmi.sizeof);
		
		bmi.bmiHeader.biSize = bmi.bmiHeader.sizeof;
		bmi.bmiHeader.biBitCount = 24;
		bmi.bmiHeader.biWidth = dest_x;
		bmi.bmiHeader.biHeight = -dest_y;
		bmi.bmiHeader.biPlanes = 1;
		bmi.bmiHeader.biCompression = 0;
		
		r = GetDIBits(dest_dc, dest_bm, 0, dest_x, buffer.ptr, &bmi, 0);
		debug writefln("r = %d GLE: %d", r, GetLastError());
		
		DeleteObject(dest_bm);
		DeleteDC(dest_dc);
		
		DeleteObject(bitmap);
		DeleteDC(mem_dc);
		ReleaseDC(null, screen_dc);
		
		Transfer trans = openTransfer();
		trans.write(get_bytes(dest_x) ~ get_bytes(dest_y));
		trans.write(buffer);
		trans.finish();
		write_msg(SCREEN_CAP, "trans_id", trans.transfer_id);
	}
	
	private void dir_list(char[] dir)
	{		
		DirEntry[] dirs = new DirEntry[1024];
		int len = 0;
		
		bool callback(DirEntry* de)
		{
			dirs[len++] = *de;
			if (len > (dirs.length*3)/4)
			{
				dirs.length = dirs.length + 1024;
			}
			return true;
		}
		
		ubyte[] de_to_buf(DirEntry *de)
		{
			static ubyte[512] buf;
			ubyte *ptr = buf.ptr;
			char[] fname = de.name.getBaseName();
			
			/*
			 * layout:
			 * is_dir			1 byte
			 * filename_len		2 bytes
			 * filename			filename_len bytes
			 * create_time		8 bytes
			 * accessed_time	8 bytes
			 * file_size		8 bytes
			 * attributes		4 bytes
			 * 
			 */
			
			//is_dir
			*ptr = de.isdir() ? 1 : 0;
			ptr++;
			//filename_len
			//filename will never be long enough to trigger buffer overflow
			*cast(ushort *)ptr = fname.length;
			ptr += ushort.sizeof;
			//filename
			ptr[0..fname.length] = cast(ubyte[])fname;
			ptr += fname.length;
			//create time
			*cast(d_time *)ptr = de.creationTime;
			ptr += d_time.sizeof;
			//modified time
			*cast(d_time *)ptr = de.lastAccessTime;
			ptr += d_time.sizeof;
			//file_size
			*cast(ulong *)ptr = de.size;
			ptr += ulong.sizeof;
			*cast(uint *)ptr = de.attributes;
			ptr += uint.sizeof;
			
			uint len = ptr - buf.ptr;
			return buf[0..len];
		}
		
		debug writefln("callback...");
		listdir(dir, &callback);
		debug writefln("callback complete.");
		if (len == 0)
		{
			raise("invalid directory selected");
			return;
		}
		int idx = 0;
		
		class WriteInfo
		{
			DirEntry[] dirs;
			int idx;
			
			this(DirEntry[] dirs)
			{
				this.dirs = dirs;
			}
		}
		
		bool write_dg(Transfer trans, Object state)
		{
			WriteInfo wi = cast(WriteInfo)state;		
			trans.write(de_to_buf(&wi.dirs[wi.idx++]));
			return wi.idx < wi.dirs.length;
		}
		auto trans = openAsyncTransfer(&write_dg, new WriteInfo(dirs[0..len]));		
		write_msg(LIST_DIR, "dir", dir, "trans_id", trans.transfer_id);
	}
	
	override protected void on_connect(int err) {
		if (err != 0)
			throw new SocketException("error on connect()");
		writefln("control channel connected");
	}
	
	override protected void on_close(int err) {
		writefln("control channel closed (%d)", err);
	}
}